# All content Copyright 2010 Cyrus Omar <cyrus.omar@gmail.com> unless otherwise
# specified.
#
# Contributors:
#     Cyrus Omar <cyrus.omar@gmail.com>
#
# This file is part of, and licensed under the terms of, the atomic-hedgehog
# package.
#
# The atomic-hedgehog package is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# The atomic-hedgehog package is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with the atomic-hedgehog package. If not, see <http://www.gnu.org/licenses/>.

"""Utility functions."""
# NOTE: should be independent of the rest of ahh. No imports from ahh.*.

import sys
import re
import types

class Error(Exception):
    """Base class for errors in the ahh.util package."""

def warn(message):
    """Issues a warning.

    See source.
    """
    print "Warning: " % message

class Version(object):
    """Generic version descriptor.

    See source.
    """
    def __init__(self, name, version, tags=None):
        self.name = name
        self.version = version
        self.version_dict = dict(version)
        if tags is None: tags = ()
        self.tags = tags

    @property
    def tag_str(self):
        return " ".join(self.tags)

    @property
    def version_num(self):
        return ".".join((ver for desc, ver in self.version))

    @property
    def version_tuple(self):
        return ( ver for desc, ver in self.version )

    @property
    def __str__(self):
        return "%s %s %s" % (self.name, self.version_num, self.tag_str)

    @property
    def full_version_num(self):
        return "\n".join(("%s: %s" % (desc, str(ver)) for desc, ver in
                          self.version))

    @property
    def full_str(self):
        return "\n".join((str(self),
                          self.full_version_num,
                          "Tags: " + self.tag_str))

version = Version("ahh.util", [ ("Major", 1), ("Minor", 0), ], ["beta"])

## General Python utilities
def all_unique(sequence):
    """Returns whether all the elements in sequence are unique."""
    list_version = list(sequence)
    return len(set(list_version)) == len(list_version)

def is_numeric(value):
    """Returns whether value is numeric."""
    # It's unclear how to best check for numerics in Python so wrapping it
    # here so if it changes, you can just change this file.
    return is_int_like(value) or is_long_like(value) or is_float_like(value)

def is_int_like(value):
    """Returns whether the value can be used as a standard integer."""
    try:
        return int(value) == value
    except:
        return False

def is_long_like(value):
    """Returns whether the value can be used as a standard long."""
    try:
        return long(value) == value
    except:
        return False

def is_float_like(value):
    """Returns whether the value can be used as a standard float."""
    try:
        return float(value) == value
    except:
        return False

def is_sequence(value):
    """Returns whether value is a sequence."""
    try:
        # Python 2.6+
        from collections import Sequence
        return isinstance(value, Sequence)
    except:
        # Python 2.5
        from operator import isSequenceType
        return isSequenceType(value)

def is_iterable(value):
    """Returns whether value is iterable.

    Mostly like is_sequence but excludes strings.
    """
    return getattr(value, '__iter__', False)

def as_iterable(value, itertype=tuple):
    if is_iterable(value):
        return value
    else:
        return itertype((value,))

def xflattened(seq, transform):
    """An iterator for a flattened version of the provided sequence.

    The transform will be applied to seq recursively before iterating.
    """
    for val in transform(seq):
        if is_iterable(val):
            for val in xflattened(val, transform):
                yield val
        else:
            yield val

def flattened(seq):
    return xflattened(seq, lambda x: x)

def flattened_stack(stack):
    return xflattened(stack, reversed)

def generic_add(set, elem):
    if hasattr(set, 'add'):
        set.add(elem)
    elif hasattr(set, 'append'):
        set.append(elem)

def generic_remove(set, elem):
    set.remove(elem)
generic_remove_Error = (KeyError, ValueError)

def add_many(set, elems):
    if is_iterable(elems):
        for elem in elems:
            set.add(elem)
    else:
        set.add(elems)

def append_many(list, elems):
    if is_iterable(elems):
        list.extend(elems)
    else:
        list.append(elems)

def remove_many(set, elems):
    if is_iterable(elems):
        for elem in elems:
            set.remove(elem)
    else:
        set.remove(elems)

def generic_add_many(set, elems):
    if is_iterable(elems):
        for elem in elems:
            generic_add(elem)
    else:
        generic_add(elems)

def generic_remove_many(set, elem):
    if is_iterable(elems):
        for elem in elems:
            generic_remove(set, elem)
    else:
        generic_remove(elems)

def generic_remove_if_present(set, elem):
    try:
        generic_remove(set, elem)
    except generic_remove_Error:
        return False
    return True

def get_passed_arg(name, default_index, args, kwargs):
    """Given an args and kwargs list, looks for the name in kwargs first, then
    in the provided default_index in the args list for the value."""
    if kwargs.has_key(name):
        return kwargs[name]
    else:
        return args[default_index]

def func_kwargs(defn):
    """For use in making decorators. Returns the kwargs dict from the defn."""
    defaults = defn.func_defaults
    if defaults is None:
        return { }

    return dict(zip(defn.func_code.co_varnames, defn.func_defaults))

def str_to_file(str, filename, mode='w'):
    f = open(filename, mode)
    f.write(str)
    f.close()

class attr_lookup:
    """Redirects dict lookups to the attributes of the provided object.

    The main use is to allow the % operator to be used with objects, e.g.

    class Test:
       a = 5
       b = "test"

       @property
       def c(self): return "C"

    >>> o = Test()
    >>> print "%(a)d, %(b)s, %(c)s" % attr_lookup(o)
    5, test, C
    """
    def __init__(self, obj):
        self.obj = obj

    def __getitem__(self, key):
        return getattr(self.obj, key)

def string_escape(string):
    """Turns special characters into escape sequences in the provided string.

    Supports both byte strings and unicode strings properly. Any other values
    will produce None.

    Example:
    >>> string_escape("a line\t")
    "a line\\t"
    >>> string_escape(u"some fancy character: \u9999")
    u"\\u9999"
    >>> string_escape(5)
    None
    """
    if isinstance(string, str):
        return string.encode("string-escape")
    elif isinstance(string, unicode):
        return unicode(string.encode("unicode-escape"))
    else:
        return None

def str_repeat(string, n):
    return "".join([string]*n)

def is_senior_subclass(obj, cls, testcls):
    """Determines whether the cls is the senior subclass of basecls for obj.

    The most senior subclass is the first class in the mro which is a subclass
    of testcls.

    Use for inheritance scheme where a method should only be called once by the
    most senior subclass.

    Don't use if it is optional to call the base method!

    In that case, the most reasonable way to do it is to ensure the method is
    idempotent and deal with the hopefully small inefficiency of calling it
    multiple times. Doing a hasattr check in these implementations should be
    relatively efficient.

    In particular, __init__ for mixins tends to act like this.
    """
    for base in obj.__class__.mro():
        if base is cls:
            return True
        else:
            if issubclass(base, testcls):
                return False

def ceil_int(num):
    return int(ceil(num))

def floor_int(num):
    return int(floor(num))

def float_div(num1, num2):
    return float(num1)/num2

def int_div_round_up(i1, i2):
    return ceil_int(float_div(i1, i2))

##
class Naming(object):
    """A mixin to provide naming operations.

    basename
        Unmangled name requested.

    _Naming_parent
        The parent object (must implement Naming too) or None

    _Naming_prefix
        What to use as a prefix in generate_unique_name.
        Defaults to self.name + "_".

    name [calculated]
        The fully namespaced name. If _Naming_parent is None, basename.
        Otherwise, _Naming_parent.generate_unique_name(basename) when
        you ask for the name for the first time.

    Methods: generate_unique_name
    """
    def __init__(self, basename=None):
        if not hasattr(self, 'basename'):
            self.__basename = basename
        if not hasattr(self, 'name'):
            self.__name = None
        if not hasattr(self, 'generate_unique_name'):
            self.__counts = 0

    @property
    def basename(self):
        return self.__basename
    if 0: isinstance(basename, str)

    @property
    def name(self):
        __name = self.__name
        if __name is None:
            parent = self._Naming_parent
            basename = self.basename
            if parent is None:
                if basename is None:
                    __name = self.__name = None
                else:
                    __name = self.__name = basename
            else:
                __name = self.__name = \
                       parent.generate_unique_name(basename)
        return __name
    if 0: isinstance(name, str)

    @property
    def _Naming_parent(self):
        if hasattr(self, 'parent') \
           and hasattr(self.parent, 'generate_unique_name'):
            return self.parent
        else:
            return None

    def generate_unique_name(self, basename):
        """Generates a unique name based on the provided basename,

        See source for details.
        """
        assert basename is not None
        assert not is_int_like(basename)

        counts = self.__counts
        if basename in counts:
            count = counts[basename]
        else:
            count = counts[basename] = 0
        counts[basename] += 1

        name = self.name
        if name is None:
            prefix = ""
        else:
            prefix = name + "_"

        if count == 0:
            return prefix + basename
        else:
            return prefix + "_" + str(count)

unique_name_generator = Naming()
"""A common unique name generator."""

## Trees
class UpTree(object):
    """A node in a tree referencing only its parent."""
    def __init__(self, parent=None, add_to_parent=True):
        if not hasattr(self, 'parent'):
            self.set_parent(parent, add_to_parent)

    def set_parent(self, parent, addremove=True):
        if addremove:
            prev = getattr(self, 'parent', None)
            if prev is not None:
                generic_remove_if_present(prev, self)
        self.__parent = parent
        if addremove:
            if parent is not None and hasattr(parent, 'children'):
                children = parent.children
                if self not in children:
                    generic_add(children, self)

    @property
    def parent(self):
        return self.__parent

    @property.setter(parent)
    def set_parent(self, parent):
        self.set_parent(parent, True)

    @property.deleter(parent)
    def del_parent(self):
        del self.__parent

    @property
    def root(node):
        """The root of the tree (traverse parents until None found)."""
        parent = self.parent
        if parent is None:
            return self
        else:
            return parent.root

class DownTree(object):
    """A node in a tree referencing only its children."""
    def __init__(self, children=None):
        if not hasattr(self, 'children'):
            if children is None:
                children = [ ]
            self.__children = children

    @property
    def children(self):
        return self.__children

class BidirectionalTree(UpTree, DownTree):
    def __init__(self, parent, add_to_parent=True, children=None):
        UpTree.__init__(self, parent, add_to_parent)
        DownTree.__init__(self, children)

## Calling hooks (recursive via tree structure)
def call_hook(root, name, *args, **kwargs):
    """Call a method recursively in the children trees if defined."""
    method = getattr(root, name, None)
    if hasattr(method, '__call__'):
        method(*args, **kwargs)

    children = getattr(root, 'children', None)
    if children is not None:
        for child in children:
            call_hook(children, name, *args, **kwargs)

def call_staged_hook(root, name, *args, **kwargs):
    """Calls a three-staged hook. See source."""
    call_hook(root, "_pre_" + name, *args, **kwargs)
    call_hook(root, "_" + name, *args, **kwargs)
    call_hook(root, "_post_" + name, *args, **kwargs)

## Instance property injection
def define_property(obj, name, fget=None, fset=None, fdel=None, doc=None):
    if hasattr(fget, 'fget'):  # can pass a property declaration too
        prop = fget
    else:
        prop = property(fget, fset, fdel, doc)
    cls = obj.__class__
    obj.__class__ = type(cls.__name__, (cls, ), {
        '__doc__': cls.__doc__,
        name: prop
    })

def define_properties(obj, props):
    cls = obj.__class__
    dct = {
        '__doc__': cls.__doc_
    }

    for name, defn in props.iteritems():
        if not hasattr(defn, 'fget'):
            defn = property(*defn)
        dct[name] = defn

    obj.__class__ = type(cls.__name__, (cls, ), dct)

## Initialized properties
# (Like regular @property, but the property can be initialized with the instance.)
_HIP_INITIALIZE_ME = '_ahh_util_HasInitializedProperties_initialize_me'
_HIP_INITIALIZED = '_ahh_util_HasInitializedProperties_initialized'
_HIP_FINIT = '_ahh_util_HasInitializedProperties_finit'

class HasInitializedProperties(type):
    """A metaclass which augments __init__ to enable initialized_property."""
    def __init__(cls, name, bases, dct):
        initialize_me = { }
        for name, val in dct.iteritems():
            if hasattr(val, 'fget'):
                val = val.fget
                if hasattr(val, _HIP_FINIT):
                    initialize_me[name] = getattr(val, _HIP_FINIT)

        if initialize_me:
            setattr(cls, _HIP_INITIALIZE_ME, initialize_me)

            if '__init__' in dct:
                prev_init = dct['__init__']

            def __init__(self, *args, **kwargs):
                if prev_init is not None:
                    prev_init(self, *args, **kwargs)
                else:
                    super(self.__class__, self).__init__(*args, **kwargs)

                init_initialized_properties(self)

            cls.__init__ = __init__

def init_initialized_properties(self):
    if not getattr(self, _HIP_INITIALIZED, False):
        setattr(self, _HIP_INITIALIZED, True)

        initialize_me = { }
        for base in reversed(self.__class__.mro()):
            initialize_me.update(getattr(base, _HIP_INITIALIZE_ME,
                                         tuple()))

        for finit in initialize_me.itervalues():
            finit(self)

def initialized_property(fget, fset=None, fdel=None, finit=None, doc=None):
    """A property which is initialized by calling finit following __init__.

    Requires HasInitializedProperties to be a metaclass or for the initializer
    to call init_initialized_properties after initialization.
    """
    if hasattr(fget, 'fget'):  # can pass in a property decl as the first arg
        prop = fget
        setattr(fget.fget, _HIP_FINIT, finit)
    else:
        prop = property(fget, fset, fdel, doc)
        setattr(fget, _HIP_FINIT, finit)

    return prop
